﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace TravellerLib {
  /// <summary>
  /// Data storage for a Traveller sector.  Stores the name and subsectors.
  /// </summary>
  public class Sector {
    /// <summary>
    /// Gets or sets the name of this sector.
    /// </summary>
    /// <value>
    /// The name of this sector.
    /// </value>
    public string Name { get; set; }
    /// <summary>
    /// Gets or sets the credits.
    /// </summary>
    /// <value>
    /// The credits.
    /// </value>
    public string Credits { get; set; }
    /// <summary>
    /// Gets or sets the author.
    /// </summary>
    /// <value>
    /// The author.
    /// </value>
    public string Author { get; set; }
    /// <summary>
    /// Gets or sets the publisher.
    /// </summary>
    /// <value>
    /// The publisher.
    /// </value>
    public string Publisher { get; set; }
    /// <summary>
    /// Gets or sets the source.
    /// </summary>
    /// <value>
    /// The source.
    /// </value>
    public string Source { get; set; }
    /// <summary>
    /// Gets or sets the reference URL.
    /// </summary>
    /// <value>
    /// The reference URL.
    /// </value>
    public string Reference { get; set; }
    private SubSector[] _subSectors;
    private Dictionary<string, string> _allegiances;
    /// <summary>
    /// A dictionary of system allegiances.
    /// </summary>
    /// <value>
    /// The allegiances.
    /// </value>
    public IDictionary<string, string> Allegiances { get { return _allegiances; } }
    private List<Route> _routes;
    public IEnumerable<Route> Routes { get { return _routes; } }
    /// <summary>
    /// Initializes a new instance of the <see cref="Sector" /> class.
    /// </summary>
    /// <param name="name">The name.</param>
    /// <param name="names">The <see cref="SubSector"/> names.</param>
    /// <remarks>
    /// Subsector names are processed starting with the most spinward and coreward:
    /// 
    /// +---+---+---+---+
    /// | A | B | C | D |
    /// +---+---+---+---+
    /// | E | F | G | H |
    /// +---+---+---+---+
    /// | I | J | K | L |
    /// +---+---+---+---+
    /// | M | N | O | P |
    /// +---+---+---+---+
    /// </remarks>
    public Sector(string name = "", IEnumerable<string> names = null) {
      Name = name;
      _subSectors = new SubSector[16];
      int i = 0;
      if (names != null)
        foreach (string subName in names) {
          _subSectors[i] = new SubSector(this, subName, (char)('A' + i));
          i++;
        }
      for (; i < 16; i++)
        _subSectors[i] = new SubSector(this, "", (char)('A' + i));
      _allegiances = new Dictionary<string, string>();
      _routes = new List<Route>();
    }

    /// <summary>
    /// Gets or sets the <see cref="SubSector" /> with the specified index.
    /// </summary>
    /// <value>
    /// The <see cref="SubSector" /> to set.
    /// </value>
    /// <param name="index">The index.</param>
    /// <returns>The <see cref="SubSector"/> at the given index.</returns>
    /// <exception cref="System.IndexOutOfRangeException">Index must be in the range of 0-15</exception>
    public SubSector this[int index] {
      get {
        if (index < 0 || index > 15)
          throw new IndexOutOfRangeException("Index must be in the range of 0-15");
        return _subSectors[index];
      }
      set {
        if (index < 0 || index > 15)
          throw new IndexOutOfRangeException("Index must be in the range of 0-15");
        _subSectors[index] = value;
      }
    }
    /// <summary>
    /// Gets or sets the <see cref="SubSector" /> at the specified index.
    /// </summary>
    /// <value>
    /// The <see cref="SubSector" /> to set.
    /// </value>
    /// <param name="index">The index.</param>
    /// <returns>The <see cref="SubSector"/> at the given index.</returns>
    /// <exception cref="System.IndexOutOfRangeException">Index must be in the range 'a'-'p' or 'A'-'P'.</exception>
    public SubSector this[char index] {
      get {
        index = char.ToLower(index); // allows both upper and lower case characters
        try {
          return this[index - 'a'];
        } catch (IndexOutOfRangeException) { // so we can change the error message appropriately.
          throw new IndexOutOfRangeException("Index must be in the range 'a'-'p' or 'A'-'P'.");
        }
      }
      set {
        index = char.ToLower(index);
        try {
          this[index - 'a'] = value;
        } catch (IndexOutOfRangeException) { // so we can change the error message appropriately.
          throw new IndexOutOfRangeException("Index must be in the range 'a'-'p' or 'A'-'P'.");
        }
      }
    }
    /// <summary>
    /// Gets or sets the <see cref="SubSector" /> at the specified index.
    /// </summary>
    /// <value>
    /// The <see cref="SubSector" /> to set.
    /// </value>
    /// <param name="index">The index.</param>
    /// <returns>The <see cref="SubSector"/> at the given index.</returns>
    /// <exception cref="System.IndexOutOfRangeException">Index must be a string of length 1.</exception>
    public SubSector this[string index] {
      get {
        if (index.Length != 1)
          throw new IndexOutOfRangeException("Index must be a string of length 1.");
        try {
          return this[index[0]];
        } catch (IndexOutOfRangeException) {
          throw new IndexOutOfRangeException("Index must be in the range \"a\"-\"p\" or \"A\"-\"P\"");
        }
      }
      set {
        if (index.Length != 1)
          throw new IndexOutOfRangeException("Index must be a string of length 1.");
        try {
          this[index[0]] = value;
        } catch (IndexOutOfRangeException) {
          throw new IndexOutOfRangeException("Index must be in the range \"a\"-\"p\" or \"A\"-\"P\"");
        }
      }
    }
    /// <summary>
    /// Gets the sub sectors.
    /// </summary>
    /// <value>
    /// The sub sectors.
    /// </value>
    public IEnumerable<SubSector> SubSectors { get { return _subSectors.AsEnumerable(); } }
    /// <summary>
    /// Gets the systems.
    /// </summary>
    /// <value>
    /// The systems.
    /// </value>
    public IEnumerable<StarSystem> Systems { get { return _subSectors.SelectMany(x => x.Systems.Values); } }
    /// <summary>
    /// Returns the <see cref="StarSystem"/> at a specified hex or null if none is present.
    /// </summary>
    /// <param name="hex">The hex.</param>
    /// <returns>The system or null.</returns>
    public StarSystem SystemAt(string hex) {
      return Systems.Where(x => x.Hex == hex).FirstOrDefault();
    }
    /// <summary>
    /// Returns the <see cref="StarSystem"/> at a specified coordinate or null if none is present.
    /// </summary>
    /// <param name="x">The x.</param>
    /// <param name="y">The y.</param>
    /// <returns>The system or null.</returns>
    public StarSystem SystemAt(int x, int y) {
      string hex = TravellerUtils.CartesianToTraveller(x, y);
      return Systems.Where(s => s.Hex == hex).FirstOrDefault();
    }
    /// <summary>
    /// Returns the <see cref="StarSystem"/> at a specified coordinate or null if none is present.
    /// </summary>
    /// <param name="coord">The coordinate.</param>
    /// <returns>The system or null.</returns>
    public StarSystem SystemAt(Tuple<int,int> coord) {
      string hex = TravellerUtils.CartesianToTraveller(coord);
      return Systems.Where(s => s.Hex == hex).FirstOrDefault();
    }

    /// <summary>
    /// Returns a <see cref="System.String" /> that represents this instance.
    /// </summary>
    /// <returns>
    /// A <see cref="System.String" /> that represents this instance.
    /// </returns>
    public override string ToString() {
      return "Sector: " + Name;
    }

    /// <summary>
    /// Loads a sector from SEC and (optional) MSEC files, with optional name override.
    /// </summary>
    /// <param name="sectorFile">The sector file.</param>
    /// <param name="metadataFile">The metadata file.</param>
    /// <param name="name">The name.</param>
    /// <returns>The sector.</returns>
    public static Sector LoadSector(string sectorFile, string metadataFile = null, string name = null) {
      Sector sector = new Sector();
      if (name != null)
        sector.Name = name;
      else // we're not passed a sector name, so sector name is the same as the SEC file name
        sector.Name = Path.GetFileNameWithoutExtension(sectorFile);
      string ext, tf;
      if (metadataFile == null) {
        ext = Path.GetExtension(sectorFile);
        tf = Path.ChangeExtension(sectorFile, ".msec");
        if (File.Exists(tf))
          metadataFile = tf;
        else if (ext.ToLower() != ".txt") {
          tf = Path.ChangeExtension(sectorFile, ".txt");
          if (File.Exists(tf))
            metadataFile = tf;
        }
      }

      StreamReader reader = new StreamReader(sectorFile);
      string[] lines = reader.ReadToEnd().Split('\n', '\r').Where(x => x != "" && x != null).ToArray(); // read in the file, split on NLs and CRs, remove blank lines
      reader.Close();

      bool foundName = false;
      foreach (string line in lines) {
        if (line.StartsWith("#")) { // comment line
          if (!foundName && line.StartsWith("# Name: ")) { // name comment
            sector.Name = line.Substring(8); // use the name from the comment
            foundName = true; // we only want to do this once, for the first occurrence
          }
          if (Regex.IsMatch(line, "# Subsector [a-pA-P]:"))  // subsector name
            sector[line.Substring(12)[0]].Name = line.Substring(15);
          if (line.StartsWith("# Credits:"))
            sector.Credits = Regex.Match(line, @"# Credits:\s+(.*?)$").Groups[1].Value;
          if (line.StartsWith("# Author:"))
            sector.Author = Regex.Match(line, @"# Author:\s+(.*?)$").Groups[1].Value;
          if (line.StartsWith("# Publisher:"))
            sector.Publisher = Regex.Match(line, @"# Publisher:\s+(.*?)$").Groups[1].Value;
          if (line.StartsWith("# Source:"))
            sector.Source = Regex.Match(line, @"# Source:\s+(.*?)$").Groups[1].Value;
          if (line.StartsWith("# Ref:"))
            sector.Reference = Regex.Match(line, @"# Ref:\s+(.*?)$").Groups[1].Value;
        } else {
          if (Regex.IsMatch(line, @"^(.{15,}) +(\d\d\d\d) +(\w\w\w\w\w\w\w-\w) +(\w| ) +(.{15,}) +(\w| ) +([0-9][0-9A-F][0-9A-F]) +(\w\w) (.*)$")) {
            Match m = Regex.Match(line, @"^(.{15,}) +(\d\d\d\d) +(\w\w\w\w\w\w\w-\w) +(\w| ) +(.{15,}) +(\w| ) +([0-9][0-9A-F][0-9A-F]) +(\w\w) (.*)$");
            string sysName = m.Groups[1].Value.TrimEnd(' ');
            Tuple<int, int> coord = TravellerUtils.TravellerToCartesian(m.Groups[2].Value);
            int secNum = 4 * ((coord.Item2 - 1) / 10) + ((coord.Item1 - 1) / 8);
            StarSystem system = new StarSystem(sysName, m.Groups[2].Value, m.Groups[3].Value, m.Groups[4].Value, m.Groups[5].Value, m.Groups[6].Value == "R" ? Zone.Red : (m.Groups[6].Value == "A" ? Zone.Amber : Zone.Normal), m.Groups[7].Value, m.Groups[8].Value);
            sector[secNum].Systems.Add(coord, system);
          }
        }
      }

      if (metadataFile != null) {
        reader = new StreamReader(metadataFile);
        lines = reader.ReadToEnd().Split('\n', '\r').Where(x => x != "" && x != null).ToArray();
        reader.Close();

        foreach (string line in lines) {
          if (line.StartsWith("#")) // comment, ignored
            continue;
          if (line.StartsWith("route ")) // route description
            sector._routes.Add(new Route(line));
        }
      }

      return sector;
    }
  }
}
